<?php

// ------------------------------------------------------------------------
// |@Author       : Jarmin <edshop@qq.com>
// |@----------------------------------------------------------------------
// |@Date         : 2025-04-06 09:31:10
// |@----------------------------------------------------------------------
// |@LastEditTime : 2025-04-29 17:54:35
// |@----------------------------------------------------------------------
// |@LastEditors  : Jarmin <jarmin@ladmin.cn>
// |@----------------------------------------------------------------------
// |@Description  : JWT认证中间件，提供基于Token的身份验证机制
// |@----------------------------------------------------------------------
// |@FilePath     : JwtAuthMiddleware.php
// |@----------------------------------------------------------------------
// |@Copyright (c) 2025 http://www.ladmin.cn   All rights reserved.
// ------------------------------------------------------------------------
declare (strict_types=1);

namespace think\admin\middleware;

use think\Request;
use think\Response;
use think\Exception;
use think\admin\service\JWT;
use think\exception\HttpResponseException;

/**
 * JWT认证中间件
 *
 * 主要功能：
 * 1. 验证JWT令牌有效性
 * 2. 处理白名单路由跳过验证
 * 3. 统一异常处理和错误响应
 * 4. 注入用户信息到请求对象
 */
class JwtAuthMiddleware
{
    protected $serviceJWT;

    public function __construct(JWT $serviceJWT)
    {
        $this->serviceJWT = $serviceJWT;
    }
    
    /**
     * 中间件处理入口
     *
     * @param Request $request 请求对象
     * @param \Closure $next 下一个中间件闭包
     * @return Response
     */
    public function handle(Request $request, \Closure $next)
    {
        try {
            // 检查白名单路由
            if ($this->shouldPassThrough($request)) {
                return $next($request);
            }

            // 执行JWT验证流程
            $this->authenticate($request);

            return $next($request);
        } catch (HttpResponseException $e) {
            return $e->getResponse();
        } catch (\Throwable $e) {
            return $this->handleException($e);
        }
    }

    /**
     * JWT认证流程
     *
     * @param Request $request 请求对象
     * @throws HttpResponseException 认证失败时抛出
     */
    protected function authenticate(Request $request): void
    {
        // 获取并验证令牌
        $token = $this->extractBearerToken($request);
        $secret = $this->getJwtSecret();

        try {
            // 解码令牌并注入用户信息
            $decoded = $this->serviceJWT->decode($token, $secret, ['HS256', 'HS384', 'HS512']);
            $request->user = (array)$decoded;
        } catch (\Exception $e) {
            // 使用ThinkPHP的异常处理
            throw new HttpResponseException(
                $this->getErrorMessage($e),
                $this->getErrorCode($e)
            );
        }
    }

    /**
     * 检查白名单路由
     *
     * @param Request $request 请求对象
     * @return bool 是否跳过验证
     */
    protected function shouldPassThrough(Request $request): bool
    {
        $path = '/' . trim($request->baseUrl(), '/');
        $excludes = config('jwt.excludes', []);

        foreach ($excludes as $pattern) {
            $pattern = '/' . trim($pattern, '/');
            // 转换通配符为正则表达式
            $regex = str_replace('\*', '.*', preg_quote($pattern, '#'));
            if (preg_match("#^{$regex}$#i", $path)) {
                return true;
            }
        }
        return false;
    }

    /**
     * 从请求头提取Bearer Token
     *
     * @param Request $request 请求对象
     * @return string JWT令牌
     * @throws HttpResponseException 令牌不存在时抛出
     */
    protected function extractBearerToken(Request $request): string
    {
        $header = $request->header('Authorization', '');
        if (!preg_match('/Bearer\s+(\S+)/i', $header, $matches)) {
            throw new HttpResponseException(lang('request.jwt_missing_token'), 40101);
        }
        return $matches[1];
    }

    /**
     * 获取JWT密钥
     *
     * @return string JWT密钥
     * @throws HttpResponseException 配置错误时抛出
     */
    protected function getJwtSecret(): string
    {
        $secret = config('jwt.secret');
        if (empty($secret)) {
            throw new HttpResponseException(lang('system.error_config'), 50001);
        }
        return $secret;
    }

    /**
     * 统一异常处理
     *
     * @param \Throwable $e 捕获的异常
     * @return Response 错误响应
     */
    protected function handleException(\Throwable $e): Response
    {
        return $this->buildErrorResponse($e);
    }

    /**
     * 构建错误响应
     *
     * @param \Throwable $e 异常对象
     * @return Response 标准化错误响应
     */
    protected function buildErrorResponse(\Throwable $e): Response
    {
        $code = $this->getErrorCode($e);
        $message = $this->getErrorMessage($e);

        return Response::create([
            'code' => $code,          // 业务错误码
            'message' => $message,    // 错误信息
            'data' => null,           // 响应数据
            'timestamp' => time()      // 时间戳
        ], 'json', $this->getHttpStatusCode($code))
            ->header(['Content-Type' => 'application/json']);
    }

    /**
     * 获取错误码
     *
     * @param \Throwable $e 异常对象
     * @return int 业务错误码
     */
    protected function getErrorCode(\Throwable $e): int
    {
        if ($e instanceof HttpResponseException) {
            return $e->getCode();
        }
        
        // 根据异常消息判断错误类型
        $message = $e->getMessage();
        if (strpos($message, 'expired') !== false) {
            return 40102;  // 令牌过期
        }
        
        return 40100;  // 默认认证错误
    }

    /**
     * 获取错误消息
     *
     * @param \Throwable $e 异常对象
     * @return string 错误信息
     */
    protected function getErrorMessage(\Throwable $e): string
    {
        // 开发模式显示原始错误
        if (config('app.debug')) {
            return $e->getMessage();
        }

        return match($this->getErrorCode($e)) {
            40101 => lang('request.jwt_missing_token'),  // 令牌缺失
            40102 => lang('request.jwt_token_expired'), // 令牌过期
            50001 => lang('request.jwt_config_error'),  // 配置错误
            default => lang('request.jwt_unauthorized') // 未授权访问
        };
    }

    /**
     * 获取HTTP状态码
     *
     * @param int $code 业务错误码
     * @return int HTTP状态码
     */
    protected function getHttpStatusCode(int $code): int
    {
        return match(true) {
            $code >= 500 => 500,  // 服务器错误
            $code >= 400 => 401,  // 未授权
            default => 400        // 错误请求
        };
    }
}
